Skip to content

Develop#8

Open
y-aithnini wants to merge 7 commits intomasterfrom
develop
Open

Develop#8
y-aithnini wants to merge 7 commits intomasterfrom
develop

Conversation

@y-aithnini
Copy link
Copy Markdown

Summary

  • What does this PR change?

Why

  • Why is this change needed?

Checklist

  • Added/updated tests (if behavior changed)
  • npm run lint passes
  • npm run typecheck passes
  • npm test passes
  • npm run build passes
  • Added a changeset (npx changeset) if this affects consumers

Notes

  • Anything reviewers should pay attention to?

Zaiidmo and others added 7 commits April 1, 2026 20:55
* feat(COMPT-50): implement Zod-based env schema validation engine

- Add defineConfig(schema) returning ConfigDefinition<T>
- Add ConfigDefinition.parse() — validates process.env synchronously,
  returns frozen fully-typed config, throws ConfigValidationError on failure
- Add ConfigValidationError extending Error with fields: ZodIssue[]
- Add zod as peerDependency (^3 || ^4)
- Install zod as devDependency for local development
- Update src/index.ts to export defineConfig, ConfigDefinition, ConfigValidationError
- Add node to tsconfig types array for process.env access

* chore: bump version to 0.1.0

* style: apply prettier formatting across all files

* fix(lint): use import type for z in define-config.ts
* feat(COMPT-50): implement Zod-based env schema validation engine

- Add defineConfig(schema) returning ConfigDefinition<T>
- Add ConfigDefinition.parse() — validates process.env synchronously,
  returns frozen fully-typed config, throws ConfigValidationError on failure
- Add ConfigValidationError extending Error with fields: ZodIssue[]
- Add zod as peerDependency (^3 || ^4)
- Install zod as devDependency for local development
- Update src/index.ts to export defineConfig, ConfigDefinition, ConfigValidationError
- Add node to tsconfig types array for process.env access

* chore: bump version to 0.1.0

* style: apply prettier formatting across all files

* fix(lint): use import type for z in define-config.ts

* feat(COMPT-51): implement ConfigModule and ConfigService

- Add ConfigModule with register() sync, registerAsync() async factory,
  and forRoot() global registration
- Add ConfigService<TDef> with typed get<K>(key) — return type inferred
  directly from Zod schema output, no casting needed
- Add CONFIG_VALUES_TOKEN internal DI token in constants.ts
- Validation runs before any provider resolves in all three registration modes
- App fails to boot when validation fails (ConfigValidationError thrown)
- Update src/index.ts to export ConfigModule, ConfigService, ConfigModuleAsyncOptions
* implemented namespaced config

* fixed prettier error

* fix(lint): fix import order in config.module.ts
* implemented test suite

* style: apply prettier formatting across all files
* readme changeset

* fixed prettier and bumped version
* removed template files and added new tests

* fix: use import type for InferConfig in spec

* fixed tag issue
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR repurposes the repo from a NestJS module template into @ciscode/config-kit, introducing a Zod-based configuration system (root + namespaced slices) for NestJS that validates process.env at startup and provides typed access via ConfigService.

Changes:

  • Replaced template “Example*” module/service/controller/repository boilerplate with ConfigKit’s defineConfig, ConfigModule, ConfigService, and defineNamespace APIs.
  • Added new error types (ConfigValidationError, DuplicateNamespaceError) and comprehensive Jest coverage for the new runtime behavior.
  • Updated package metadata/docs (README/CHANGELOG), TS config, and minor CI/workflow formatting.

Reviewed changes

Copilot reviewed 30 out of 32 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
tsconfig.json Adds Node types to support process.env typing in the new config implementation/tests.
src/index.ts Updates public exports to expose ConfigKit APIs and stop exporting template artifacts.
src/errors/config-validation.error.ts Adds a custom startup validation error carrying Zod issues.
src/errors/config-validation.error.spec.ts Adds unit tests for ConfigValidationError.
src/define-config.ts Introduces defineConfig() / ConfigDefinition.parse() and InferConfig helper.
src/define-config.spec.ts Tests parse behavior (defaults, coercion, error paths, immutability).
src/constants.ts Adds internal DI tokens and namespace token generator.
src/config.service.ts Adds typed ConfigService with .get() and .getAll().
src/config.module.ts Adds dynamic module APIs (register, registerAsync, forRoot) and namespace registry wiring.
src/config.module.spec.ts Integration tests for module registration modes and service behavior.
src/decorators/inject-config.decorator.ts Adds @InjectConfig() decorator for namespaced slice injection.
src/define-namespace.ts Adds per-feature namespace config (defineNamespace, NamespacedConfig, duplicate detection error).
src/define-namespace.spec.ts Integration tests for namespacing and DI integration.
README.md Rewrites documentation for ConfigKit usage and API reference.
CHANGELOG.md Introduces a package changelog for ConfigKit.
package.json Renames package to @ciscode/config-kit and updates dependencies/peer deps.
package-lock.json Updates lockfile to reflect dependency changes (incl. zod).
jest.config.ts Raises global coverage thresholds to 85%.
.github/workflows/release-check.yml Formatting-only changes (quote style).
.github/workflows/publish.yml Formatting-only changes (quote style).
.github/dependabot.yml Formatting-only changes (quote style).
.changeset/thick-maps-raise.md Removes an old template changeset.
.changeset/config.json Updates changesets repo metadata for the new repository/package.
src/services/example.service.ts Removes template service (no longer relevant).
src/example-kit.module.ts Removes template module (replaced by ConfigModule).
src/controllers/example.controller.ts Removes template controller.
src/guards/example.guard.ts Removes template guard.
src/repositories/example.repository.ts Removes template repository.
src/entities/example.entity.ts Removes template entity.
src/dto/create-example.dto.ts Removes template DTO.
src/dto/update-example.dto.ts Removes template DTO.
src/decorators/example.decorator.ts Removes template decorator.
Comments suppressed due to low confidence (1)

package.json:56

  • @nestjs/platform-express is listed as a peer dependency even though the package code doesn't import it; this unnecessarily forces consumers onto the Express platform (and complicates Fastify apps). Also, class-transformer/class-validator are runtime dependencies but appear unused in src/ now—keeping them increases install surface area. Consider removing unused deps/peer deps (or making platform peer optional) to match actual runtime requirements.
  "peerDependencies": {
    "@nestjs/common": "^10 || ^11",
    "@nestjs/core": "^10 || ^11",
    "@nestjs/platform-express": "^10 || ^11",
    "reflect-metadata": "^0.2.2",
    "rxjs": "^7",
    "zod": "^3 || ^4"
  },
  "dependencies": {
    "class-transformer": "^0.5.1",
    "class-validator": "^0.14.1"
  },

Comment thread src/config.module.ts
Comment on lines +173 to +175
@Global() // Applied only when used via forRoot(); register() and registerAsync() are non-global
@Module({})
export class ConfigModule {
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Global() on the module class makes ConfigModule global for all import styles (register/registerAsync/forRoot). This contradicts the inline comment and the returned DynamicModule objects setting global: false, and can unintentionally leak ConfigService across modules. Consider removing @Global() and relying solely on global: true in forRoot(), or otherwise update the implementation/docs so register/registerAsync are truly non-global.

Copilot uses AI. Check for mistakes.
Comment thread src/config.module.ts
Comment on lines +293 to +315
private static createAsyncConfigProvider<T extends AnyZodObject>(
options: ConfigModuleAsyncOptions<T>,
): Provider {
if (options.useFactory) {
// ── useFactory ─────────────────────────────────────────────────────────
return {
provide: CONFIG_VALUES_TOKEN,
useFactory: async (...args: unknown[]) => {
// Call the consumer's factory to get the ConfigDefinition
const definition = await options.useFactory!(...args);
// Parse and validate synchronously — throws on bad env before app boots
return definition.parse(process.env);
},
inject: (options.inject ?? []) as never[],
};
}

// ── useClass / useExisting ────────────────────────────────────────────────
// Both patterns delegate to ConfigModuleOptionsFactory.createConfigDefinition()
// useClass → NestJS creates a fresh instance of the class
// useExisting → NestJS reuses the already-registered instance
const factoryToken = (options.useClass ?? options.useExisting)!;

Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

createAsyncConfigProvider() assumes that if useFactory is not provided, then useClass or useExisting is set (via a non-null assertion). If a consumer passes invalid options (none or multiple of these fields), this will fail with a confusing runtime error. Add explicit validation in registerAsync() (or here) to enforce exactly one of useFactory/useClass/useExisting and throw a clear error message when the contract is violated.

Copilot uses AI. Check for mistakes.
Comment thread src/define-config.ts
Comment on lines +72 to +99
* @param env - The environment record to validate.
* Defaults to `process.env` so consumers never need to pass it.
* @returns A deep-frozen, fully-typed config object with no `string | undefined` values.
* Zod coerces types and fills in `.default()` values automatically.
* @throws {ConfigValidationError} When one or more fields fail validation.
* The error lists every failing ZodIssue so developers can fix all
* problems in a single restart cycle.
*
* @example
* ```typescript
* // Called internally by ConfigModule — you rarely call this yourself
* const config = appConfig.parse(process.env);
* config.PORT; // string (never undefined)
* config.DATABASE_URL; // string
* ```
*/
parse(env: Record<string, string | undefined> = process.env): Readonly<z.output<T>> {
// Run a safe (non-throwing) parse so we can collect ALL issues at once
const result = this.schema.safeParse(env);

// If validation failed, throw with the full ZodIssue list
if (!result.success) {
throw new ConfigValidationError(result.error.issues);
}

// Freeze the resulting object so consumers cannot mutate config at runtime
return Object.freeze(result.data) as Readonly<z.output<T>>;
}
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The JSDoc says parse() returns a "deep-frozen" config object, but the implementation only uses Object.freeze(result.data), which is shallow and allows mutation of nested objects/arrays. Either implement a deep-freeze (recursive) before returning, or update the docs to reflect shallow immutability and any guarantees/limitations.

Copilot uses AI. Check for mistakes.
Comment thread src/define-namespace.ts
Comment on lines +240 to +245
export function defineNamespace<T extends AnyZodObject>(
namespace: string,
schema: T,
): NamespacedConfig<T> {
// No side effects at definition time — validation is deferred to asProvider()
return new NamespacedConfig(namespace, schema);
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

defineNamespace() accepts any string for namespace (including empty/whitespace), which would generate ambiguous DI tokens (e.g. CONFIG_KIT_NS:) and make duplicate detection/error messages confusing. Consider validating namespace (e.g. non-empty after trim, possibly restrict characters) and throwing a clear error early.

Copilot uses AI. Check for mistakes.
Comment thread CHANGELOG.md
Comment on lines +1 to +8
# @ciscode/config-kit

## 0.3.0

### Minor Changes

- Added ConfigService.getAll(), InferConfig<T> type helper, useClass/useExisting support in registerAsync, and removed template boilerplate from the published package

Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The package version is 0.3.1 (package.json) but the changelog's latest entry is 0.3.0. Add a 0.3.1 section or adjust versioning so CHANGELOG and published version stay in sync.

Copilot uses AI. Check for mistakes.
Comment thread README.md
Comment on lines +239 to +241
| Method | Returns |
| ----------------- | --------------------------------------------------------------------- |
| `config.get(key)` | Zod output type for `key` — never `undefined` unless schema allows it |
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

README API reference for ConfigService<TDef> only documents get(key), but the PR introduces getAll() and exports it as part of the public service API. Please document getAll() here (and, if intended, clarify whether it returns the same frozen reference or a copy).

Suggested change
| Method | Returns |
| ----------------- | --------------------------------------------------------------------- |
| `config.get(key)` | Zod output type for `key` — never `undefined` unless schema allows it |
| Method | Returns | Notes |
| ------------------- | --------------------------------------------------------------------- | --------------------------------------------------------------------- |
| `config.get(key)` | Zod output type for `key` — never `undefined` unless schema allows it | Reads a single validated config value by key |
| `config.getAll()` | Fully validated config object for `TDef` | Returns the full config object; this README does not guarantee whether it is the same frozen reference or a copy |

Copilot uses AI. Check for mistakes.
Comment thread README.md
Comment on lines +231 to +235
| Method | Description |
| ------------------------------------- | ---------------------------------------------------------- |
| `ConfigModule.forRoot(definition)` | Global registration — `ConfigService` available everywhere |
| `ConfigModule.register(definition)` | Non-global registration — scoped to the importing module |
| `ConfigModule.registerAsync(options)` | Async registration with `useFactory` / `inject` |
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the API reference table for ConfigModule, registerAsync(options) is described only in terms of useFactory/inject, but the module now also supports useClass and useExisting patterns. Consider updating the README to reflect all supported option shapes so consumers don't miss these capabilities.

Suggested change
| Method | Description |
| ------------------------------------- | ---------------------------------------------------------- |
| `ConfigModule.forRoot(definition)` | Global registration — `ConfigService` available everywhere |
| `ConfigModule.register(definition)` | Non-global registration — scoped to the importing module |
| `ConfigModule.registerAsync(options)` | Async registration with `useFactory` / `inject` |
| Method | Description |
| ------------------------------------- | ----------------------------------------------------------------------------- |
| `ConfigModule.forRoot(definition)` | Global registration — `ConfigService` available everywhere |
| `ConfigModule.register(definition)` | Non-global registration — scoped to the importing module |
| `ConfigModule.registerAsync(options)` | Async registration with `useFactory` / `inject`, `useClass`, or `useExisting` |

Copilot uses AI. Check for mistakes.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants